在 vue 实例调用 mountComponent 挂载时,会实例化一个渲染 Watcher。当响应式数据更新时,通知渲染 Watcher,调用回调函数 _update。

function mountComponent (
  vm,
  el,
  hydrating
) {
  vm.$el = el;
  // ...
    
  var updateComponent;
  /* istanbul ignore if */
  if (config.performance && mark) {
    updateComponent = function () {
      // ...
      vm._update(vnode, hydrating); // 调用 _update 函数执行 __patch__
      // ...
    };
  } else {
    updateComponent = function () {
      vm._update(vm._render(), hydrating); // 调用 _update 函数执行 __patch__
    };
  }

  // 渲染 Watcher
  new Watcher(vm, updateComponent, noop, {
    before: function before() {
      if (vm._isMounted && !vm._isDestroyed) {
        callHook(vm, 'beforeUpdate');
      }
    }
  }, true /* isRenderWatcher */);
  hydrating = false;

  // ...
  return vm
}

_update 函数在 lifecycleMixin 时已挂载到 Vue 原型上,在 _update 函数内部会调用 __patch__ 对元素节点进行更新。

function lifecycleMixin (Vue) {
  Vue.prototype._update = function (vnode, hydrating) {
    var vm = this;
    var prevEl = vm.$el;
    var prevVnode = vm._vnode;
    var restoreActiveInstance = setActiveInstance(vm);
    vm._vnode = vnode;
    
    // 调用 __patch__ 更新元素节点
    if (!prevVnode) {
      // initial render
      vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */);
    } else {
      // updates
      vm.$el = vm.__patch__(prevVnode, vnode);
    }
    restoreActiveInstance();
    // update __vue__ reference
    if (prevEl) {
      prevEl.__vue__ = null;
    }
    if (vm.$el) {
      vm.$el.__vue__ = vm;
    }
    // if parent is an HOC, update its $el as well
    if (vm.$vnode && vm.$parent && vm.$vnode === vm.$parent._vnode) {
      vm.$parent.$el = vm.$el;
    }
  };
}

__patch__ 函数挂载在 Vue 原型上,并且只有在浏览器端才会有 patch 逻辑,因为 patch 最终更新的是实际的 DOM 节点。

Vue.prototype.__patch__ = inBrowser ? patch : noop;
var patch = createPatchFunction({ nodeOps: nodeOps, modules: modules });
function createPatchFunction (backend) {
  // ...
  
  return function patch(oldVnode, vnode, hydrating, removeOnly) {
    // ...
    return vnode.elm
  }
}

由以上源码可知,__patch__ 函数最终调用的是 createPatchFunction 函数返回的 patch 函数。

# patch 函数

patch 函数有四个参数:

  • oldVnode:旧 vnode 数据
  • vnode:新 vnode 数据
  • hydrating:是否为服务端渲染。
  • removeOnly:是否只执行移除逻辑。

函数源码:

function patch(oldVnode, vnode, hydrating, removeOnly) {
    // 新 vnode 为空,说明该 vnode 已删除,调用 oldVnode 的 destroy hook
    if (isUndef(vnode)) {
      if (isDef(oldVnode)) { invokeDestroyHook(oldVnode); }
      return
    }

    var isInitialPatch = false;
    var insertedVnodeQueue = [];

    if (isUndef(oldVnode)) {
      // 旧 vnode 为空,直接根据新 vnode 创建元素节点
      isInitialPatch = true;
      createElm(vnode, insertedVnodeQueue);
    } else {
      var isRealElement = isDef(oldVnode.nodeType);
      if (!isRealElement && sameVnode(oldVnode, vnode)) {
        // 通过 sameVnode 判断新旧 vnode 为相同节点,调用 patchVnode 更新旧 Vnode
        patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly);
      } else {
        if (isRealElement) {
          // 判断是否为服务端渲染
          if (oldVnode.nodeType === 1 && oldVnode.hasAttribute(SSR_ATTR)) {
            oldVnode.removeAttribute(SSR_ATTR);
            hydrating = true;
          }
          if (isTrue(hydrating)) {
            // 服务端渲染客户端激活逻辑
            if (hydrate(oldVnode, vnode, insertedVnodeQueue)) { // 客户端激活
              invokeInsertHook(vnode, insertedVnodeQueue, true); // 执行客户端相关逻辑,如事件绑定等
              return oldVnode
            } else {
              warn(
                'The client-side rendered virtual DOM tree is not matching ' +
                'server-rendered content. This is likely caused by incorrect ' +
                'HTML markup, for example nesting block-level elements inside ' +
                '<p>, or missing <tbody>. Bailing hydration and performing ' +
                'full client-side render.'
              );
            }
          }
          // 如果不是服务端渲染,或者客户端激活失败,则根据旧 Vnode 创建空 Vnode
          oldVnode = emptyNodeAt(oldVnode);
        }
        // 旧节点更新完成之后,替换元素节点
        var oldElm = oldVnode.elm;
        var parentElm = nodeOps.parentNode(oldElm);

        // 创建新节点
        createElm(
          vnode,
          insertedVnodeQueue,
          // extremely rare edge case: do not insert if old element is in a
          // leaving transition. Only happens when combining transition +
          // keep-alive + HOCs. (#4590)
          oldElm._leaveCb ? null : parentElm,
          nodeOps.nextSibling(oldElm)
        );

        // 递归更新父节点的占位节点
        if (isDef(vnode.parent)) {
          var ancestor = vnode.parent;
          var patchable = isPatchable(vnode);
          while (ancestor) {
            for (var i = 0; i < cbs.destroy.length; ++i) {
              cbs.destroy[i](ancestor);
            }
            ancestor.elm = vnode.elm;
            if (patchable) {
              for (var i$1 = 0; i$1 < cbs.create.length; ++i$1) {
                cbs.create[i$1](emptyNode, ancestor);
              }
              // #6513
              // invoke insert hooks that may have been merged by create hooks.
              // e.g. for directives that uses the "inserted" hook.
              var insert = ancestor.data.hook.insert;
              if (insert.merged) {
                // start at index 1 to avoid re-invoking component mounted hook
                for (var i$2 = 1; i$2 < insert.fns.length; i$2++) {
                  insert.fns[i$2]();
                }
              }
            } else {
              registerRef(ancestor);
            }
            ancestor = ancestor.parent;
          }
        }

        // 没有父节点,销毁旧节点
        if (isDef(parentElm)) {
          removeVnodes([oldVnode], 0, 0);
        } else if (isDef(oldVnode.tag)) {
          invokeDestroyHook(oldVnode);
        }
      }
    }
		// 调用 invokeInsertHook
    invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch);
    return vnode.elm
  }

执行逻辑:

  • 首先判断如果新旧 vnode 是否为空:
    • 如果新 vnode 为空,说明该 vnode 已删除,调用 oldVnode 的 destroy hook。
    • 如果旧 vnode 为空,直接根据新 vnode 创建元素节点。
  • 接下来判断旧 vnode 是否为真实的 DOM 节点:
    • 如果不是,说明旧 vnode 是 vnode 数据;接着通过 sameVnode 函数判断新旧 vnode 是否相同,如果相同则调用 patchVnode 函数进行更新。
    • 如果是,判断是否为服务端渲染,如果是则执行客户端激活逻辑。调用 hydrate 函数对实际的节点进行更新和调用 invokeInsertHook 激活客户端代码,如事件绑定等。

# patchVnode 函数

函数参数为:

  • oldVnode: 旧 vnode。
  • vnode:新 vnode。
  • insertedVnodeQueue:待插入 vnode 队列。
  • ownerArray:源 vnode 数据,用于复用克隆 vnode 使用。
  • index:当前 vnode 索引。
  • removeOnly:是否只执行移除逻辑。

函数源码:

function patchVnode (
    oldVnode,
    vnode,
    insertedVnodeQueue,
    ownerArray,
    index,
    removeOnly
  ) {
    // 新旧 vnode 相同,直接返回
    if (oldVnode === vnode) {
      return
    }

    if (isDef(vnode.elm) && isDef(ownerArray)) {
      // clone reused vnode
      vnode = ownerArray[index] = cloneVNode(vnode);
    }

    var elm = vnode.elm = oldVnode.elm;

    if (isTrue(oldVnode.isAsyncPlaceholder)) {
      if (isDef(vnode.asyncFactory.resolved)) {
        hydrate(oldVnode.elm, vnode, insertedVnodeQueue);
      } else {
        vnode.isAsyncPlaceholder = true;
      }
      return
    }

    // 复用静态节点
    if (isTrue(vnode.isStatic) &&
      isTrue(oldVnode.isStatic) &&
      vnode.key === oldVnode.key &&
      (isTrue(vnode.isCloned) || isTrue(vnode.isOnce))
    ) {
      vnode.componentInstance = oldVnode.componentInstance;
      return
    }

    var i;
    var data = vnode.data;
    // 调用 prepacth,更新子组件的 propsData、listeners,触发子组件 render-watcher 的 setter,进而更新子组件
    if (isDef(data) && isDef(i = data.hook) && isDef(i = i.prepatch)) {
      i(oldVnode, vnode);
    }

    var oldCh = oldVnode.children;
    var ch = vnode.children;
    if (isDef(data) && isPatchable(vnode)) {
      // 执行 cbs.update 中的所有函数
      for (i = 0; i < cbs.update.length; ++i) { cbs.update[i](oldVnode, vnode); }
      if (isDef(i = data.hook) && isDef(i = i.update)) { i(oldVnode, vnode); }
    }
    if (isUndef(vnode.text)) {
      // 非文本节点
      if (isDef(oldCh) && isDef(ch)) {
        // 新旧 vnode 都有子节点,执行 updateChildren 更新子节点
        if (oldCh !== ch) { updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly); }
      } else if (isDef(ch)) {
        // 新 vnode 有子节点而旧 vnode 没有子节点,检验 key 值后将新 vnode 的子节点添加到插入队列 insertedVnodeQueue 中
        {
          checkDuplicateKeys(ch);
        }
        if (isDef(oldVnode.text)) { nodeOps.setTextContent(elm, ''); }
        addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue);
      } else if (isDef(oldCh)) {
        // 旧 vnode 有子节点而新 vnode 没有子节点,直接删除旧 vnode 的子节点
        removeVnodes(oldCh, 0, oldCh.length - 1);
      } else if (isDef(oldVnode.text)) {
        // 新旧 vnode 都有没有子节点,但是旧 vnode 有文本,则置空文本
        nodeOps.setTextContent(elm, '');
      }
    } else if (oldVnode.text !== vnode.text) {
      // 是文本节点且新旧 vnode 的文本内容不相同,则直接替换实际元素节点上的文本内容
      nodeOps.setTextContent(elm, vnode.text);
    }
    if (isDef(data)) {
      // 调用 postpatch 函数
      if (isDef(i = data.hook) && isDef(i = i.postpatch)) { i(oldVnode, vnode); }
    }
  }

函数执行逻辑如下:

  1. 判断新旧 vnode 是否相同,相同的话直接返回。

  2. 判断新旧 vnode 是否为静态节点,是的话复用静态节点。

  3. 调用 prepacth,更新子组件的 propsData、listeners,触发子组件 render-watcher 的 setter,触发子组件更新逻辑,即:触发子组件的 _update 函数。

  4. 执行 cbs.update 中的所有函数。

  5. 判断新 vnode 是否为文本节点,如果非文本节点

    1. 新旧 vnode 都有子节点,执行 updateChildren 更新子节点,也就是 diff patch 的执行逻辑。
    2. 新 vnode 有子节点而旧 vnode 没有子节点,检验 key 值后将新 vnode 的子节点添加到插入队列 insertedVnodeQueue 中。
    3. 旧 vnode 有子节点而新 vnode 没有子节点,直接删除旧 vnode 的子节点。
    4. 新旧 vnode 都有没有子节点,但是旧 vnode 有文本,则置空文本。
  6. 判断新 vnode 是否为文本节点,如果是文本节点且新旧 vnode 的文本内容不相同,则直接替换实际元素节点上的文本内容

  7. 执行 patch 逻辑完成之后的 postpatch 函数。

以上就是 patchVnode 的执行逻辑,接下来看 updateChildren 函数,也就是 diff patch 的执行逻辑。

# updateChildren 函数

函数参数如下:

  • parentElm:父 DOM 元素。
  • oldCh:旧子列表。
  • newCh:新子列表。
  • insertedVnodeQueue:待插入 vnode 队列。
  • removeOnly:是否只执行移除逻辑。
function updateChildren (parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) {
    var oldStartIdx = 0; // 旧子 vnode 列表头指针
    var newStartIdx = 0; // 新子 vnode 列表头指针
    var oldEndIdx = oldCh.length - 1; // 旧子 vnode 列表尾指针
    var oldStartVnode = oldCh[0]; // 旧子 vnode 列表首元素
    var oldEndVnode = oldCh[oldEndIdx]; // 旧子 vnode 列表尾元素
    var newEndIdx = newCh.length - 1; // 新子 vnode 列表尾指针
    var newStartVnode = newCh[0]; // 新子 vnode 列表首元素
    var newEndVnode = newCh[newEndIdx]; // 新子 vnode 列表首元素
    var oldKeyToIdx, // Map 数据结构,key-index 用于存放旧 vnode 的 key 和 vnode-index 的映射关系,用于判断新 vnode 是否可以复用
      idxInOld, // 存放查找新 vnode 在旧 vnode 列表中的索引
      vnodeToMove,
      refElm;

    // removeOnly is a special flag used only by <transition-group>
    // to ensure removed elements stay in correct relative positions
    // during leaving transitions
    var canMove = !removeOnly;

    {
      // 校验 key 值
      checkDuplicateKeys(newCh);
    }

    // 遍历新旧子 vnode 列表,直到其中一个遍历结束为止
    while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
      // 头尾指针双端两两比较,各个情况如下:

      if (isUndef(oldStartVnode)) {
        // 向右查找旧子 vnode 列表第一个有 vnode 数据的节点(首元素)
        oldStartVnode = oldCh[++oldStartIdx]; // Vnode has been moved left
      } else if (isUndef(oldEndVnode)) {
        // 向左查找旧子 vnode 列表第一个有 vnode 数据的节点(尾元素)
        oldEndVnode = oldCh[--oldEndIdx];
      } else if (sameVnode(oldStartVnode, newStartVnode)) {
        // 新旧子 vnode 列表各自的头指针指向的 vnode(首元素)相同,调用 patchVnode 函数更新
        // 旧头指针右移
        // 新头指针右移
        patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx);
        oldStartVnode = oldCh[++oldStartIdx];
        newStartVnode = newCh[++newStartIdx];
      } else if (sameVnode(oldEndVnode, newEndVnode)) {
        // 新旧子 vnode 列表各自的尾指针指向的 vnode(尾元素)相同,调用 patchVnode 函数更新
        // 旧尾指针左移
        // 新尾指针左移
        patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx);
        oldEndVnode = oldCh[--oldEndIdx];
        newEndVnode = newCh[--newEndIdx];
      } else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right
        // 旧子 vnode 列表的首元素 和 新子 vnode 列表的尾元素相同,说明该 vnode 被移动到右边
        // 调用 patchVnode 更新
        patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx);
        // 在旧 vnode 的尾元素所在的实际 DOM 元素后面插入更新后的旧 vnode 所在的实际 DOM 元素
        canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm));
        // 旧头指针右移
        oldStartVnode = oldCh[++oldStartIdx];
        // 新尾指针左移
        newEndVnode = newCh[--newEndIdx];
      } else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left
        // 旧子 vnode 列表的尾元素 和 新子 vnode 列表的首元素相同,说明该 vnode 被移动到左边
        // 调用 patchVnode 更新
        patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx);
        // 在旧 vnode 的头元素所在的实际 DOM 元素前面插入更新后的旧 vnode 所在的实际 DOM 元素
        canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm);
        // 旧尾指针左移
        oldEndVnode = oldCh[--oldEndIdx];
        // 新头指针右移
        newStartVnode = newCh[++newStartIdx];
      } else {
        // 头尾指针双端两两比较之后,其他情况如下:

        // 如果没有旧 vnode 的 key-index Map,则调用 createKeyToOldIdx 创建该 Map 
        if (isUndef(oldKeyToIdx)) { oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx); }
        // 通过新 vnode 的 key 值在 oldKeyToIdx 中查找是否在旧 vnode 中存在该 vnode
        // 查找是否在旧 vnode 中存在该 vnode
        // 1. 在 oldKeyToIdx 中查找
        // 2. 在 oldKeyToIdx 中查找不到,则再在旧 vnode 列表中遍历查找
        idxInOld = isDef(newStartVnode.key)
          ? oldKeyToIdx[newStartVnode.key]
          : findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx);
        if (isUndef(idxInOld)) {
          // 旧 vnode 列表中无当前新 vnode,创建新 DOM 节点并添加到旧头元素所在的实际 DOM 元素之前
          createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx);
        } else {
          // 旧 vnode 列表中存在当前新 vnode,说明节点被移动了
          vnodeToMove = oldCh[idxInOld];
          // 判断是否为相同 vnode
          if (sameVnode(vnodeToMove, newStartVnode)) {
            // 新旧 vnode 为相同 vnode,则调用 patchVnode 函数更新 oldVnode
            patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue, newCh, newStartIdx);
            oldCh[idxInOld] = undefined; // 将移动前的旧 vnode 设置为 undefined
            // 在旧首元素之前插入更新后的 oldVnode 实际节点
            canMove && nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm);
          } else {
            // same key but different element. treat as new element
            // 相同 key 值但是新旧 vnode 为不同 vnode,创建新 DOM 节点并添加到旧头元素所在的实际 DOM 元素之前
            createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx);
          }
        }
        // 新头指针右移
        newStartVnode = newCh[++newStartIdx];
      }
    }

    if (oldStartIdx > oldEndIdx) {
      // 旧头指针移动到旧尾指针后面,说明旧子 vnode 列表已经遍历结束,且新子 vnode 列表长度大于旧子 vnode 列表
      // 将新子 vnode 列表中剩下的 vnode 添加到新尾元素所在的实际 DOM 元素之后
      refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm;
      addVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx, insertedVnodeQueue);
    } else if (newStartIdx > newEndIdx) {
      // 新头指针移动到新尾指针后面,说明新子 vnode 列表已经遍历结束,且旧子 vnode 列表长度大于旧子 vnode 列表
      // 移除旧子 vnode 列表中剩下的 vnode
      removeVnodes(oldCh, oldStartIdx, oldEndIdx);
    }
  }

updateChildren 的核心逻辑为:

  • 采用双指针法,对新旧 vnode 列表进行双端两两比较,即:新首旧首、新尾旧尾、新首旧尾、新尾旧首。
  • 在进行双端两两比较之后,其他情况则会创建旧 vnode 列表的 Map 数据结构 oldKeyToIdx,用于存放 vnode 的 key-index 的映射关系。然后将新 vnode 的 key 在 oldKeyToIdx 中查找,找不到则再在旧 vnode 列表中查找,最终找得到则判断是否相同 vnode,是的话则在旧首元素之前插入节点;在 oldKeyToIdx 中找不到或者找得到但新旧 vnode 不为相同 vnode,则都直接创建新的元素节点。
  • 当新旧 vnode 的 while 循环遍历结束:
    • 如果旧头指针移动到旧尾指针后面,说明旧子 vnode 列表已经遍历结束,且新子 vnode 列表长度大于旧子 vnode 列表,就需要将新子 vnode 列表中剩下的 vnode 添加到新尾元素所在的实际 DOM 元素之后。
    • 如果新头指针移动到新尾指针后面,说明新子 vnode 列表已经遍历结束,且旧子 vnode 列表长度大于旧子 vnode 列表,则需要移除旧子 vnode 列表中剩下的 vnode。

另外,由于在通过 sameVnode 判断为同一 vnode 后,会对递归调用 patchVnode 函数。所以该算法是深度优先遍历,找到同层 vnode 列表再进行 diff。

# sameVnode 函数

判断两个 vnode 是否相同的源码如下:

function sameVnode (a, b) {
  return (
    a.key === b.key &&
    a.asyncFactory === b.asyncFactory && (
      (
        a.tag === b.tag &&
        a.isComment === b.isComment &&
        isDef(a.data) === isDef(b.data) &&
        sameInputType(a, b)
      ) || (
        isTrue(a.isAsyncPlaceholder) &&
        isUndef(b.asyncFactory.error)
      )
    )
  )
}

# 举个例子

下面为开始数据状态,DOM 和旧 Vnode 列表相同。

image-20220601221812062

第一次,oldStartVnode 与 newEndVnode 相同,则:在实际 DOM 中将 oldStartIdx 的节点移动到 oldEndIdx 的节点的右边,同时,oldStartIdx 右移,newEndIdx 左移。

image-20220601221838989

第二次,oldEndVnode 与 newStartVnode 相同,则:在实际 DOM 中将 oldEndIdx 的节点移动到 oldStartIdx 的节点的左边,同时,oldEndIdx 左移,newStartIdx 右移。

image-20220601222223839

第三次,oldStartVnode 与 newStartVnode 相同,oldStartIdx 右移,newStartIdx 右移。

image-20220601222617874

第四次,newStartVnode 存在于旧 vnode 列表中,在实际 DOM 中将 newStartIdx 的节点移动到旧头指针实际节点之前,并将旧列表中该 vnode 设置为 undefined,同时 newStartIdx 右移。

image-20220601222743909

第五次,oldEndVnode 与 newStartVnode 相同,则:在实际 DOM 中将 oldEndIdx 的节点移动到 oldStartIdx 的节点的左边,同时,oldEndIdx 左移,newStartIdx 右移。

image-20220601223037326

第六次,新 startVnode 不在旧 vnode 列表中,则将该节点插入到旧头元素代表的实际节点的前面。

image-20220601223122953

第七次,newStartIdx > newEndIdx,说明旧 vnode 列表长度大于新 vnode 列表长度,直接将旧 vnode 列表剩余节点在实际 DOM 中存在的节点删掉。

image-20220601223227511